package ray.surface;

import ray.Intersection;
import ray.Ray;
import ray.math.Point2;
import ray.math.Point3;
import ray.math.Vector2;
import ray.math.Vector3;
import ray.shader.Shader;

/**
 * Represents a sphere as a center and a radius.
 * 
 * @author ags
 */
public class TriangleMesh extends Surface {
    /** The normal vector of this triangle mesh */
    private Vector3 normal;

    /** The mesh that contains this triangle mesh */
    private Mesh owner;

    /** 3 indices to the vertices of this triangle. */
    private int[] index = new int[3];

    public TriangleMesh(Mesh owner, int index0, int index1, int index2, Shader material) {
        this.owner = owner;
        
        index[0] = index0;
        index[1] = index1;
        index[2] = index2;

        Point3 pointA = owner.getVertex(index[0]);
        Point3 pointB = owner.getVertex(index[1]);
        Point3 pointC = owner.getVertex(index[2]);

        if (!owner.existsNormals()) {
            Vector3 e0 = new Vector3();
            Vector3 e1 = new Vector3();
            e0.sub(pointB, pointA);
            e1.sub(pointC, pointA);
            normal = new Vector3();
            normal.cross(e0, e1);
            normal.normalize();
        }

        this.setShader(material);
    }
    
    private Vector3 getNormal(double alpha, double beta, double gamma) {
        if (owner.existsNormals()) {
            Vector3 normalResult = new Vector3();
            final Vector3 normalPointA = owner.getNormal(index[0]);
            final Vector3 normalPointB = owner.getNormal(index[1]);
            final Vector3 normalPointC = owner.getNormal(index[2]);
            
            normalResult.scaleAdd(alpha, normalPointA);
            normalResult.scaleAdd(beta, normalPointB);
            normalResult.scaleAdd(gamma, normalPointC);
            
            normalResult.normalize();
            
            return normalResult;
        }
        
        return normal;
    }
    

    /**
     * Tests this surface for intersection with ray. If an intersection is found record is filled out with the information about
     * the intersection and the method returns true. It returns false otherwise and the information in outRecord is not modified.
     * 
     * @param outRecord
     *            the output IntersectionRecord
     * @param ray
     *            the ray to intersect
     * @return true if the surface intersects the ray
     */
    @Override
    public double findIntersection (Intersection outRecord, Ray rayIn) {
        Ray transformedRay = untransformRay(rayIn);

        // this next section is unattractive
        // we will use the variable names from the book, for simplicity
        
        // it is possible that there is a simpler and better solution in Shirley 2.6

        Point3 pointA = owner.getVertex(index[0]);
        Point3 pointB = owner.getVertex(index[1]);
        Point3 pointC = owner.getVertex(index[2]);

        double j = pointA.x - transformedRay.p.x;
        double k = pointA.y - transformedRay.p.y;
        double l = pointA.z - transformedRay.p.z;

        double a = pointA.x - pointB.x;
        double b = pointA.y - pointB.y;
        double c = pointA.z - pointB.z;

        double d = pointA.x - pointC.x;
        double e = pointA.y - pointC.y;
        double f = pointA.z - pointC.z;

        double g = transformedRay.d.x;
        double h = transformedRay.d.y;
        double i = transformedRay.d.z;

        double eiMinusHf = e * i - h * f;
        double gfMinusDi = g * f - d * i;
        double dhMinusEg = d * h - e * g;
        
        double M = a * eiMinusHf + b * gfMinusDi + c * dhMinusEg;
        
        double akMinusJb = a * k - j * b;
        double jcMinusAl = j * c - a * l;
        double blMinusKc = b * l - k * c;
        
        
        double gamma = (i * akMinusJb + h * jcMinusAl + g * blMinusKc) / M;
        
        if (gamma < 0 || gamma > 1) {
            return Double.NaN;
        }
        
        double beta = (j * eiMinusHf + k * gfMinusDi + l * dhMinusEg) / M;
        
        if (beta < 0 || beta > 1 - gamma) {
            return Double.NaN;
        }
        
        double alpha = 1 - beta - gamma;
        
        double t = -(f * akMinusJb + e * jcMinusAl + d * blMinusKc) / M;
        
        outRecord.surface = this;
        outRecord.t = t;
        outRecord.intersection.set(transformedRay.p);
        outRecord.intersection.scaleAdd(t, transformedRay.d);
        outRecord.normal.set(getNormal(alpha, beta, gamma));

        return t;
    }
    
    public void computeBoundingBox() {
        // TODO(B): Compute the bounding box and store the result in
        // averagePosition, minBound, and maxBound.

        Point3[] vertices = new Point3[] { owner.getVertex(index[0]), owner.getVertex(index[1]), owner.getVertex(index[2]) };

        maxBound.x = maxBound.y = maxBound.z = Double.NEGATIVE_INFINITY;
        minBound.x = minBound.y = minBound.z = Double.POSITIVE_INFINITY;

        assert !(vertices[0].equals(vertices[1]) && vertices[0].equals(vertices[1])) : "All three vertices are the same.";
        
        for (Point3 vertex : vertices) {
            Point3 transformedVertex = new Point3(vertex);
            getObjToWorldTransform().rightMultiply(transformedVertex);
            
            maxBound.x = Math.max(maxBound.x, transformedVertex.x);
            maxBound.y = Math.max(maxBound.y, transformedVertex.y);
            maxBound.z = Math.max(maxBound.z, transformedVertex.z);

            minBound.x = Math.min(minBound.x, transformedVertex.x);
            minBound.y = Math.min(minBound.y, transformedVertex.y);
            minBound.z = Math.min(minBound.z, transformedVertex.z);
        }

        averagePosition.average(minBound, maxBound);
    }


    /**
     * @see Object#toString()
     */
    public String toString() {
        return "Triangle ";
    }

	@Override
	public boolean intersect(Intersection intersectionrecord, Ray ray) {
		return false;
	}
}